Skip to content

feat: Next generation Stripe billing integration#2161

Open
niemyjski wants to merge 16 commits intomainfrom
feature/next-stripe
Open

feat: Next generation Stripe billing integration#2161
niemyjski wants to merge 16 commits intomainfrom
feature/next-stripe

Conversation

@niemyjski
Copy link
Copy Markdown
Member

Summary

  • Adds new Stripe billing components: ChangePlanDialog and StripeProvider
  • Implements Stripe Checkout session creation via API
  • Adds billing features to organization API: getBilling, changePlan, getInvoices
  • Updates billing pages to use new components
  • Updates organization and invoice mappers
  • Adds Stripe integration plan document

Changes

  • New Svelte components for Stripe integration
  • New backend endpoints for billing management
  • Updated usage pages to show billing info

- Upgrade Stripe.net to v50.4.1 and update the backend to support modern PaymentMethods while maintaining legacy token compatibility.
- Implement a new billing feature in the Svelte UI with lazy-loaded Stripe integration and a functional plan change dialog.
- Add TanStack Query hooks for fetching available plans and processing plan changes with coupon support.
Copilot AI review requested due to automatic review settings March 17, 2026 03:14
@niemyjski niemyjski force-pushed the feature/next-stripe branch from 985add7 to ee79cb9 Compare March 17, 2026 03:15
@niemyjski niemyjski self-assigned this Mar 17, 2026
Comment on lines +285 to +288
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch price details for price ID: {PriceId}. Exception: {Message}", priceId, ex.Message);
}
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a “next generation” Stripe billing integration spanning backend Stripe.net v50 updates, new billing endpoints/DTO mapping, and a new Svelte 5 billing UI (Stripe provider + change-plan dialog) wired into organization/project usage and billing pages.

Changes:

  • Upgrades Stripe.net usage on the backend (invoice status handling, discounts, subscription updates, PaymentMethod support).
  • Adds a new Svelte billing feature module (StripeProvider, ChangePlanDialog) and hooks it into usage/billing routes.
  • Extends org client API with plan query + change-plan mutation; updates invoice mapping/tests accordingly.

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/Exceptionless.Tests/Mapping/InvoiceMapperTests.cs Updates Stripe invoice test data to use Status instead of Paid.
tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs Simplifies test comments (no functional change).
src/Exceptionless.Web/Mapping/InvoiceMapper.cs Maps “paid” via Invoice.Status string comparison.
src/Exceptionless.Web/Controllers/OrganizationController.cs Updates Stripe invoice retrieval/discount handling and modernizes change-plan flow to support PaymentMethods + Stripe.net 50 API changes.
src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/usage/+page.svelte “Change plan” now navigates to the org billing page.
src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/usage/+page.svelte “Change plan” now navigates to the org billing page.
src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/billing/+page.svelte Uses new billing module ChangePlanDialog and passes loaded org data into it.
src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/change-plan-dialog.svelte Removes the legacy placeholder change-plan dialog component.
src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts Adds getPlansQuery() and changePlanMutation() + related query keys/types.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/stripe.svelte.ts Adds Stripe context + lazy singleton loader utilities.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/schemas.ts Adds zod schema/types for the change-plan form.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/models.ts Adds billing model re-exports + local billing form/params types.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/index.ts Barrel exports for billing feature module.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/constants.ts Defines FREE_PLAN_ID.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/stripe-provider.svelte Adds Stripe Elements provider wrapper with loading/error states.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/change-plan-dialog.svelte Implements the plan change dialog UI including Stripe payment collection.
src/Exceptionless.Web/ClientApp/package.json Adds Stripe JS + svelte-stripe dependencies.
src/Exceptionless.Web/ClientApp/package-lock.json Locks Stripe JS + svelte-stripe dependencies.
src/Exceptionless.Web/ClientApp/STRIPE-INTEGRATION-PLAN.md Adds the Stripe integration plan document.
src/Exceptionless.Core/Exceptionless.Core.csproj Upgrades Stripe.net to 50.4.1.
.claude/agents/engineer.md Adds guidance for rerunning flaky CI jobs via gh run rerun.
Files not reviewed (1)
  • src/Exceptionless.Web/ClientApp/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while getting the invoice: {InvoiceId}", id);
_logger.LogError(ex, "An error occurred while getting the invoice: {InvoiceId}. Exception: {Message}", id, ex.Message);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch price details for price ID: {PriceId}. Exception: {Message}", priceId, ex.Message);
Copilot AI review requested due to automatic review settings April 16, 2026 04:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a “next generation” Stripe billing integration across the backend (Stripe.net 50.x) and the Svelte client, enabling plan selection/changes and invoice display with the updated Stripe API surface.

Changes:

  • Upgrades Stripe.net to 50.4.1 and updates invoice/plan-change logic to use Status, Price, Discounts, and PaymentMethod APIs.
  • Adds a new client-side billing feature module (Stripe context/provider + change plan dialog) and wires it into usage/billing pages.
  • Adds client API helpers for fetching plans and performing plan changes, with cache invalidation.

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tests/Exceptionless.Tests/Mapping/InvoiceMapperTests.cs Updates invoice mapping tests to reflect Stripe Status replacing Paid.
tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs Simplifies test comments (no functional change).
src/Exceptionless.Web/Mapping/InvoiceMapper.cs Switches paid logic to derive from Invoice.Status.
src/Exceptionless.Web/Controllers/OrganizationController.cs Updates invoice retrieval and plan change flows for Stripe.net 50.x (Price/Discounts/PaymentMethod changes).
src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/usage/+page.svelte Replaces placeholder “change plan” handler with navigation to billing page.
src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/usage/+page.svelte Same as above for org usage page.
src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/billing/+page.svelte Switches to the new billing module’s ChangePlanDialog and passes organization data.
src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/change-plan-dialog.svelte Removes the old placeholder change-plan dialog component.
src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts Adds getPlansQuery + changePlanMutation and associated query keys/types.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/stripe.svelte.ts Adds Stripe context utilities and a singleton Stripe loader.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/schemas.ts Adds Zod schema for the change-plan form.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/models.ts Adds billing-focused types and re-exports generated API models.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/index.ts Exposes billing module public API (components/hooks/constants/types).
src/Exceptionless.Web/ClientApp/src/lib/features/billing/constants.ts Introduces FREE_PLAN_ID.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/stripe-provider.svelte Adds a Stripe <Elements> provider and sets Stripe context for children.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/change-plan-dialog.svelte Implements the new plan change UX using Stripe PaymentElement + plans query + mutation.
src/Exceptionless.Web/ClientApp/package.json Adds @stripe/stripe-js and svelte-stripe.
src/Exceptionless.Web/ClientApp/package-lock.json Locks new Stripe dependencies.
src/Exceptionless.Core/Exceptionless.Core.csproj Upgrades Stripe.net from 47.4.0 to 50.4.1.
Files not reviewed (1)
  • src/Exceptionless.Web/ClientApp/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Id = source.Id[3..], // Strip "in_" prefix
Date = source.Created,
Paid = source.Paid
Paid = String.Equals(source.Status, "paid", StringComparison.Ordinal)
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while getting the invoice: {InvoiceId}", id);
_logger.LogError(ex, "An error occurred while getting the invoice: {InvoiceId}. Exception: {Message}", id, ex.Message);
OrganizationName = organization.Name,
Date = stripeInvoice.Created,
Paid = stripeInvoice.Paid,
Paid = String.Equals(stripeInvoice.Status, "paid", StringComparison.Ordinal),
Comment on lines +253 to +269
var priceService = new PriceService(client);
var priceCache = new Dictionary<string, Stripe.Price>(StringComparer.Ordinal);
foreach (var line in stripeInvoice.Lines.Data)
{
var item = new InvoiceLineItem { Amount = line.Amount / 100.0m, Description = line.Description };
if (line.Plan is not null)

// In Stripe.net 50.x, Plan was removed from InvoiceLineItem
// Fetch full Price object from Stripe to get nickname, interval, and amount
var priceId = line.Pricing?.PriceDetails?.Price;
if (!String.IsNullOrEmpty(priceId))
{
string planName = line.Plan.Nickname ?? _billingManager.GetBillingPlan(line.Plan.Id)?.Name ?? line.Plan.Id;
item.Description = $"Exceptionless - {planName} Plan ({(line.Plan.Amount / 100.0):c}/{line.Plan.Interval})";
try
{
if (!priceCache.TryGetValue(priceId, out var price))
{
price = await priceService.GetAsync(priceId);
priceCache[priceId] = price;
Comment on lines 520 to 563
var update = new SubscriptionUpdateOptions { Items = [] };
var create = new SubscriptionCreateOptions { Customer = organization.StripeCustomerId, Items = [] };
bool cardUpdated = false;

var customerUpdateOptions = new CustomerUpdateOptions { Description = organization.Name };
if (!Request.IsGlobalAdmin())
customerUpdateOptions.Email = CurrentUser.EmailAddress;

if (!String.IsNullOrEmpty(stripeToken))
{
customerUpdateOptions.Source = stripeToken;
if (isPaymentMethod)
{
// Modern Svelte UI: Attach PaymentMethod and set as default
await paymentMethodService.AttachAsync(stripeToken, new PaymentMethodAttachOptions
{
Customer = organization.StripeCustomerId
});
customerUpdateOptions.InvoiceSettings = new CustomerInvoiceSettingsOptions
{
DefaultPaymentMethod = stripeToken
};
}
else
{
// Legacy Angular UI: Use Source for token
customerUpdateOptions.Source = stripeToken;
}
cardUpdated = true;
}

await customerService.UpdateAsync(organization.StripeCustomerId, customerUpdateOptions);

var subscriptionList = await subscriptionService.ListAsync(new SubscriptionListOptions { Customer = organization.StripeCustomerId });
var subscription = subscriptionList.FirstOrDefault(s => !s.CanceledAt.HasValue);
if (subscription is not null)
{
update.Items.Add(new SubscriptionItemOptions { Id = subscription.Items.Data[0].Id, Plan = planId });
update.Items.Add(new SubscriptionItemOptions { Id = subscription.Items.Data[0].Id, Price = planId });
await subscriptionService.UpdateAsync(subscription.Id, update);
}
else
{
create.Items.Add(new SubscriptionItemOptions { Plan = planId });
create.Items.Add(new SubscriptionItemOptions { Price = planId });
await subscriptionService.CreateAsync(create);
}
Comment on lines +57 to +58
_stripeInstance = await _stripePromise;
return _stripeInstance;
Comment on lines 444 to +512
@@ -434,17 +471,45 @@

var createCustomer = new CustomerCreateOptions
{
Source = stripeToken,
Plan = planId,
Description = organization.Name,
Email = CurrentUser.EmailAddress
};

if (!String.IsNullOrWhiteSpace(couponId))
createCustomer.Coupon = couponId;
// Handle both legacy tokens and modern PaymentMethod IDs for backwards compatibility
if (isPaymentMethod)
{
// Modern Svelte UI: Uses PaymentMethod from createPaymentMethod()
createCustomer.PaymentMethod = stripeToken;
createCustomer.InvoiceSettings = new CustomerInvoiceSettingsOptions
{
DefaultPaymentMethod = stripeToken
};
}
else
{
// Legacy Angular UI: Uses token from createToken()
createCustomer.Source = stripeToken;
}

var customer = await customerService.CreateAsync(createCustomer);

// Create subscription separately (Plan on CustomerCreateOptions is deprecated)
var subscriptionOptions = new SubscriptionCreateOptions
{
Customer = customer.Id,
Items = [new SubscriptionItemOptions { Price = planId }]
};

if (isPaymentMethod)
subscriptionOptions.DefaultPaymentMethod = stripeToken;

// In Stripe.net 50.x, Coupon was removed from SubscriptionCreateOptions
// Use Discounts collection with SubscriptionDiscountOptions instead
if (!String.IsNullOrWhiteSpace(couponId))
subscriptionOptions.Discounts = [new SubscriptionDiscountOptions { Coupon = couponId }];

await subscriptionService.CreateAsync(subscriptionOptions);

Copilot AI review requested due to automatic review settings April 16, 2026 05:03
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new Stripe-based billing UX in the Svelte app and updates the backend Stripe integration to newer Stripe.net APIs, alongside some infra/telemetry adjustments.

Changes:

  • Adds new billing feature module on the frontend (Stripe provider + change-plan dialog) and wires it into usage/billing pages.
  • Updates backend invoice/payment handling to Stripe.net 50.x patterns (invoice status, discounts, line item pricing/price lookup).
  • Updates dependencies/config (Stripe.net upgrade, adds @stripe/stripe-js + svelte-stripe, adjusts OpenTelemetry Prometheus wiring and deploy workflow conditions).

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
tests/Exceptionless.Tests/Mapping/InvoiceMapperTests.cs Updates invoice mapping tests to use Status instead of Paid.
tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs Simplifies test comments (no functional change).
src/Exceptionless.Web/Startup.cs Removes Prometheus scraping endpoint middleware.
src/Exceptionless.Web/Mapping/InvoiceMapper.cs Maps Paid from Invoice.Status == "paid".
src/Exceptionless.Web/Exceptionless.Web.csproj Removes Prometheus exporter package reference; formatting changes.
src/Exceptionless.Web/Controllers/OrganizationController.cs Updates invoice and plan-change flows for Stripe.net 50.x (prices/discounts/payment methods).
src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/usage/+page.svelte Navigates to org billing page with changePlan=true.
src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/usage/+page.svelte Navigates to org billing page with changePlan=true.
src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/billing/+page.svelte Switches to new billing ChangePlanDialog and passes organization data.
src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/change-plan-dialog.svelte Deletes placeholder dialog (replaced by billing feature).
src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts Adds plans query + change-plan mutation/query keys.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/stripe.svelte.ts Adds Stripe context + lazy loader utilities.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/schemas.ts Adds Zod schema for change-plan form.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/models.ts Adds billing types and form state types.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/index.ts Adds billing feature barrel exports.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/constants.ts Adds FREE_PLAN_ID constant.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/stripe-provider.svelte Adds Stripe Elements provider component.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/change-plan-dialog.svelte Adds new Stripe-backed change-plan dialog UI.
src/Exceptionless.Web/ClientApp/package.json Adds Stripe JS dependencies.
src/Exceptionless.Web/ClientApp/package-lock.json Locks Stripe JS dependencies.
src/Exceptionless.Web/ApmExtensions.cs Removes Prometheus exporter from OpenTelemetry metrics pipeline.
src/Exceptionless.Job/Program.cs Removes Prometheus scraping endpoint middleware from job host.
src/Exceptionless.Job/Exceptionless.Job.csproj Removes Prometheus exporter package reference; formatting changes.
src/Exceptionless.Core/Exceptionless.Core.csproj Upgrades Stripe.net from 47.4.0 to 50.4.1; formatting changes.
.vscode/settings.json Updates workspace editor/TypeScript SDK settings.
.github/workflows/build.yaml Updates deploy job condition to allow dev deploys from a configured PR branch.
Files not reviewed (1)
  • src/Exceptionless.Web/ClientApp/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Exceptionless.Job/Program.cs Outdated
Comment on lines 100 to 107

app.UseHealthChecks("/health", new HealthCheckOptions
{
Predicate = hcr => !String.IsNullOrEmpty(jobOptions.JobName) ? hcr.Tags.Contains(jobOptions.JobName) : hcr.Tags.Contains("AllJobs")
Comment on lines +496 to +500
// Create subscription separately (Plan on CustomerCreateOptions is deprecated)
var subscriptionOptions = new SubscriptionCreateOptions
{
Customer = customer.Id,
Items = [new SubscriptionItemOptions { Price = planId }]
Comment on lines 552 to 562
var subscriptionList = await subscriptionService.ListAsync(new SubscriptionListOptions { Customer = organization.StripeCustomerId });
var subscription = subscriptionList.FirstOrDefault(s => !s.CanceledAt.HasValue);
if (subscription is not null)
{
update.Items.Add(new SubscriptionItemOptions { Id = subscription.Items.Data[0].Id, Plan = planId });
update.Items.Add(new SubscriptionItemOptions { Id = subscription.Items.Data[0].Id, Price = planId });
await subscriptionService.UpdateAsync(subscription.Id, update);
}
else
{
create.Items.Add(new SubscriptionItemOptions { Plan = planId });
create.Items.Add(new SubscriptionItemOptions { Price = planId });
await subscriptionService.CreateAsync(create);
Comment on lines +271 to +288
<StripeProvider
appearance={{
theme: 'stripe',
variables: {
borderRadius: '6px'
}
}}
onElementsChange={(elements) => {
stripeElements = elements;
}}
onload={(loadedStripe) => {
stripe = loadedStripe;
}}
>
<div class="rounded-lg border p-4">
<PaymentElement />
</div>
</StripeProvider>
Comment on lines 184 to 190
}
});
app.UseStatusCodePages();

app.UseOpenTelemetryPrometheusScrapingEndpoint();

app.UseHealthChecks("/health", new HealthCheckOptions
{
Predicate = hcr => hcr.Tags.Contains("Critical") || (options.RunJobsInProcess && hcr.Tags.Contains("AllJobs"))
Comment on lines 121 to 142
services.AddOpenTelemetry().WithMetrics(b =>
{
b.SetResourceBuilder(resourceBuilder);

b.AddHttpClientInstrumentation();
b.AddAspNetCoreInstrumentation();
b.AddMeter("Exceptionless", "Foundatio");
b.AddMeter("System.Runtime");
b.AddRuntimeInstrumentation();
b.AddProcessInstrumentation();

if (config.Console)
b.AddConsoleExporter((_, metricReaderOptions) =>
{
// The ConsoleMetricExporter defaults to a manual collect cycle.
// This configuration causes metrics to be exported to stdout on a 10s interval.
metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 10000;
});

b.AddPrometheusExporter();

if (config.EnableExporter)
b.AddOtlpExporter();
});
Comment on lines +253 to +270
var priceService = new PriceService(client);
var priceCache = new Dictionary<string, Stripe.Price>(StringComparer.Ordinal);
foreach (var line in stripeInvoice.Lines.Data)
{
var item = new InvoiceLineItem { Amount = line.Amount / 100.0m, Description = line.Description };
if (line.Plan is not null)

// In Stripe.net 50.x, Plan was removed from InvoiceLineItem
// Fetch full Price object from Stripe to get nickname, interval, and amount
var priceId = line.Pricing?.PriceDetails?.Price;
if (!String.IsNullOrEmpty(priceId))
{
string planName = line.Plan.Nickname ?? _billingManager.GetBillingPlan(line.Plan.Id)?.Name ?? line.Plan.Id;
item.Description = $"Exceptionless - {planName} Plan ({(line.Plan.Amount / 100.0):c}/{line.Plan.Interval})";
try
{
if (!priceCache.TryGetValue(priceId, out var price))
{
price = await priceService.GetAsync(priceId);
priceCache[priceId] = price;
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while getting the invoice: {InvoiceId}", id);
_logger.LogError(ex, "An error occurred while getting the invoice: {InvoiceId}. Exception: {Message}", id, ex.Message);
Copilot AI review requested due to automatic review settings April 16, 2026 15:28
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a next-generation Stripe-based billing flow across the Svelte app and the organization API, updating both frontend plan-change UX and backend Stripe integration to newer Stripe.net APIs.

Changes:

  • Adds a new Billing feature module (Stripe context/provider + ChangePlanDialog) and wires it into the organization billing page and usage pages.
  • Updates backend Stripe integration (Stripe.net 50.x) for invoice/payment/plan handling and updates invoice mapping/tests accordingly.
  • Updates build/ops tooling (Docker MinVer build arg, workflow tweaks) and removes Prometheus OpenTelemetry exporter/scraping.

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/Exceptionless.Tests/Mapping/InvoiceMapperTests.cs Updates tests to reflect invoice Status -> paid mapping.
tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs Simplifies test comments (no behavior change).
src/Exceptionless.Web/Startup.cs Removes OTEL Prometheus scraping endpoint middleware.
src/Exceptionless.Web/Mapping/InvoiceMapper.cs Maps invoice “paid” from Stripe Status instead of Paid.
src/Exceptionless.Web/Exceptionless.Web.csproj Removes Prometheus exporter package + formatting changes.
src/Exceptionless.Web/Controllers/OrganizationController.cs Updates Stripe invoice/plan change logic for Stripe.net 50.x and PaymentMethod support.
src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/usage/+page.svelte Implements navigation to billing page on “Change plan”.
src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/usage/+page.svelte Implements navigation to billing page on “Change plan”.
src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/billing/+page.svelte Switches to new billing ChangePlanDialog component and passes organization data.
src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/change-plan-dialog.svelte Removes placeholder dialog from organizations feature.
src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts Adds plans query + change-plan mutation and query keys.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/stripe.svelte.ts Adds Stripe lazy-loader + context hooks.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/schemas.ts Adds Zod schema for change-plan form.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/models.ts Adds billing feature types and re-exports generated API types.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/index.ts Public entrypoint for billing feature exports.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/constants.ts Adds FREE plan id constant.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/stripe-provider.svelte Adds provider wrapping Stripe Elements + loading/error UI.
src/Exceptionless.Web/ClientApp/src/lib/features/billing/components/change-plan-dialog.svelte Adds full Stripe Elements-based plan change dialog.
src/Exceptionless.Web/ClientApp/package.json Adds Stripe JS dependencies.
src/Exceptionless.Web/ClientApp/package-lock.json Locks Stripe JS dependencies.
src/Exceptionless.Web/ApmExtensions.cs Removes Prometheus exporter wiring.
src/Exceptionless.Job/Program.cs Removes OTEL Prometheus scraping endpoint middleware.
src/Exceptionless.Job/Exceptionless.Job.csproj Removes Prometheus exporter package + formatting changes.
src/Exceptionless.Core/Exceptionless.Core.csproj Upgrades Stripe.net to 50.4.1.
Dockerfile Adds MinVerVersionOverride build arg passthrough.
.vscode/settings.json Disables format-on-save and changes TypeScript SDK path setting key.
.github/workflows/build.yaml Ensures checkout uses correct ref and passes MinVer build arg into Docker builds; expands deploy conditions.
Files not reviewed (1)
  • src/Exceptionless.Web/ClientApp/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .vscode/settings.json
"svelte": "html"
},
"typescript.tsdk": "\\src\\Exceptionless.Web\\ClientApp\\node_modules\\typescript\\lib",
"js/ts.tsdk.path": "src/Exceptionless.Web/ClientApp/node_modules/typescript/lib",
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while getting the invoice: {InvoiceId}", id);
_logger.LogError(ex, "An error occurred while getting the invoice: {InvoiceId}. Exception: {Message}", id, ex.Message);
Comment on lines 121 to 142
services.AddOpenTelemetry().WithMetrics(b =>
{
b.SetResourceBuilder(resourceBuilder);

b.AddHttpClientInstrumentation();
b.AddAspNetCoreInstrumentation();
b.AddMeter("Exceptionless", "Foundatio");
b.AddMeter("System.Runtime");
b.AddRuntimeInstrumentation();
b.AddProcessInstrumentation();

if (config.Console)
b.AddConsoleExporter((_, metricReaderOptions) =>
{
// The ConsoleMetricExporter defaults to a manual collect cycle.
// This configuration causes metrics to be exported to stdout on a 10s interval.
metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 10000;
});

b.AddPrometheusExporter();

if (config.EnableExporter)
b.AddOtlpExporter();
});
Comment on lines +190 to +192
const response = await client.postJSON<ChangePlanResult>(`organizations/${request.route.organizationId}/change-plan`, undefined, {
params
});
Comment on lines +47 to +56
export interface ChangePlanParams extends Record<string, string | undefined> {
/** Optional coupon code to apply */
couponId?: string;
/** Last 4 digits of the card (for display purposes) */
last4?: string;
/** The plan ID to change to */
planId: string;
/** Stripe PaymentMethod ID or legacy token */
stripeToken?: string;
}
niemyjski and others added 5 commits April 18, 2026 10:22
Stripe v9 requires either clientSecret OR mode when calling stripe.elements().
The previous code passed neither when no clientSecret was provided, causing
PaymentElement to fail to render.

Uses a discriminated union Props type to enforce mutual exclusivity of
clientSecret and mode at compile time, matching Stripe's own type constraints.
Defaults to mode='setup' for collecting payment methods for future use.
Fix two issues preventing the Stripe PaymentElement from rendering in
the Change Plan dialog:

1. Missing currency in Stripe Elements options: Stripe.js v9+ requires
   a currency string when using mode: 'setup'. Added currency: 'usd'
   to the elements creation options.

2. svelte-stripe Svelte 5 incompatibility: svelte-stripe's <Elements>
   and <PaymentElement> components use $bindable()/onMount patterns
   that don't trigger Svelte 5 template re-renders from async callbacks.
   Replaced both components with an imperative approach that loads Stripe,
   creates elements, and mounts the PaymentElement directly to the DOM
   via onMount, bypassing Svelte's reactive template system entirely.

Additionally fixed the Change Plan dialog not being scrollable by adding
max-h-[90vh] and overflow-y-auto to the Dialog.Content wrapper.

Changes:
- stripe-provider.svelte: Rewritten to imperatively mount PaymentElement
  without svelte-stripe components or Svelte reactive conditionals
- change-plan-dialog.svelte: Removed svelte-stripe PaymentElement import,
  updated StripeProvider usage (no longer takes children), added dialog
  scroll constraints, removed debug markup

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Upgrade @stripe/stripe-js to v9.2.0.
- Update OrganizationController to use PriceId instead of Price, following changes in Stripe.net 51.x where Price became an expandable field.
- Convert errorMessage to a Svelte 5 $state rune in stripe-provider.svelte to ensure reactivity.
@github-actions
Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Exceptionless.Core 66% 60% 7629
Exceptionless.Insulation 25% 23% 203
Exceptionless.Web 57% 43% 3676
Exceptionless.AppHost 26% 14% 55
Summary 61% (11914 / 19502) 54% (5955 / 11118) 11563

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

3 participants